/*
 * Minio Go Library for Amazon S3 Compatible Cloud Storage
 * Copyright 2015-2017 Minio, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package signature

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"net/http"
	"strings"
	"unicode/utf8"
)

// getSigningKey hmac seed to calculate final signature.
func getSigningKey(secret, loc string, scopeDate string) []byte {
	date := sumHMAC([]byte("AWS4"+secret), []byte(scopeDate))
	location := sumHMAC(date, []byte(loc))
	service := sumHMAC(location, []byte("s3"))
	signingKey := sumHMAC(service, []byte("aws4_request"))
	return signingKey
}

// getSignature final signature in hexadecimal form.
func getSignature(signingKey []byte, stringToSign string) string {
	return hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign)))
}

// getScope generate a string of a specific date, an AWS region, and a
// service.
// func getScope(location string, t time.Time) string {
// 	scope := strings.Join([]string{
// 		t.Format(yyyymmdd),
// 		location,
// 		"s3",
// 		"aws4_request",
// 	}, "/")
// 	return scope
// }

// GetCredential generate a credential string.
// func GetCredential(req *http.Request) string {

// 	return accessKeyID + "/" + scope
// }

// getHashedPayload get the hexadecimal value of the SHA256 hash of
// the request payload.
func getHashedPayload(req http.Request) string {
	hashedPayload := req.Header.Get(AmzContentSha256)
	if hashedPayload == "" {
		// Presign does not have a payload, use S3 recommended value.
		hashedPayload = unsignedPayload
	}
	return hashedPayload
}

// getCanonicalHeaders generate a list of request headers for
// signature.
func getCanonicalHeaders(req http.Request, headers []string) string {
	vals := make(map[string][]string)
	for _, k := range headers {
		vals[k] = req.Header.Values(k)
	}

	var buf bytes.Buffer
	// Save all the headers in canonical form <header>:<value> newline
	// separated for each header.
	for _, k := range headers {
		buf.WriteString(k)
		buf.WriteByte(':')
		switch {
		case k == "host":
			buf.WriteString(getHostAddr(&req))
			fallthrough
		default:
			for idx, v := range vals[k] {
				if idx > 0 {
					buf.WriteByte(',')
				}
				buf.WriteString(v)
			}
			buf.WriteByte('\n')
		}
	}
	return buf.String()
}

// getSignedHeaders generate all signed request headers.
// i.e lexically sorted, semicolon-separated list of lowercase
// request header names.
func getSignedHeaders(req *http.Request) ([]string, string) {
	signHeaderStr := req.URL.Query().Get("X-Amz-SignedHeaders")
	signHeaders := strings.Split(signHeaderStr, ";")
	return signHeaders, signHeaderStr
}

// getCanonicalRequest generate a canonical request of style.
//
// canonicalRequest =
//
//	<HTTPMethod>\n
//	<CanonicalURI>\n
//	<CanonicalQueryString>\n
//	<CanonicalHeaders>\n
//	<SignedHeaders>\n
//	<HashedPayload>
func getCanonicalRequest(req http.Request, signedHeaders []string, signedHeaderValue string) string {
	req.URL.RawQuery = strings.Replace(req.URL.Query().Encode(), "+", "%20", -1)
	canonicalRequest := strings.Join([]string{
		req.Method,
		EncodePath(req.URL.Path),
		req.URL.RawQuery,
		getCanonicalHeaders(req, signedHeaders),
		signedHeaderValue,
		getHashedPayload(req),
	}, "\n")
	return canonicalRequest
}

// getStringToSign a string based on selected query values.
func getStringToSignV4(time, location, scope, canonicalRequest string) string {
	stringToSign := signV4Algorithm + "\n" + time + "\n"
	stringToSign = stringToSign + scope + "\n"
	stringToSign = stringToSign + hex.EncodeToString(sum256([]byte(canonicalRequest)))
	return stringToSign
}

// PreSignV4 presign the request, in accordance with
// http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html.
func PreSignV4(req *http.Request, accessKeyID, secretAccessKey string) {
	// Presign is not needed for anonymous credentials.
	if accessKeyID == "" || secretAccessKey == "" {
		return
	}

	query := req.URL.Query()

	t := query.Get(AmzDate)

	// Get credential string.
	ch, ac2 := parseCredentialHeader("Credential="+query.Get(AmzCredential), DefaultZone)
	if ac2 != ErrNone {
		return
	}
	query.Del(AmzSignature)
	req.URL.RawQuery = query.Encode()

	// Get all signed headers.
	signedHeaderSets, signedHeaderStr := getSignedHeaders(req)

	// Get canonical request.
	canonicalRequest := getCanonicalRequest(*req, signedHeaderSets, signedHeaderStr)

	// stringToSign := getStringToSignV4(t, sv.Credential.scope.region, sv.Credential.scopeWithoutAk, canonicalRequest)
	// Get string to sign from canonical request.
	stringToSign := getStringToSignV4(t, ch.scope.region, ch.scopeWithoutAk, canonicalRequest)

	// Gext hmac signing key.
	signingKey := getSigningKey(secretAccessKey, ch.scope.region, ch.scope.dateStr)

	// Calculate signature.
	signature := getSignature(signingKey, stringToSign)

	query.Set(AmzSignature, signature)

	// Add signature header to RawQuery.
	req.URL.RawQuery = query.Encode()
}

func ValidaPreSign(req *http.Request, accessKeyID, secretAccessKey string) bool {
	cloneReq := req.Clone(req.Context())
	// Presign is not needed for anonymous credentials.
	if accessKeyID == "" || secretAccessKey == "" {
		return false
	}

	query := cloneReq.URL.Query()

	t := query.Get(AmzDate)

	// Get credential string.
	ch, ac2 := parseCredentialHeader("Credential="+query.Get(AmzCredential), DefaultZone)
	if ac2 != ErrNone {
		return false
	}

	originSignature := query.Get(AmzSignature)

	query.Del(AmzSignature)
	cloneReq.URL.RawQuery = query.Encode()

	// Get all signed headers.
	signedHeaderSets, signedHeaderStr := getSignedHeaders(cloneReq)

	// Get canonical request.
	canonicalRequest := getCanonicalRequest(*cloneReq, signedHeaderSets, signedHeaderStr)

	// Get string to sign from canonical request.
	stringToSign := getStringToSignV4(t, ch.scope.region, ch.scopeWithoutAk, canonicalRequest)

	// Gext hmac signing key.
	signingKey := getSigningKey(secretAccessKey, ch.scope.region, ch.scope.dateStr)

	// Calculate signature.
	return getSignature(signingKey, stringToSign) == originSignature
}

// PostPresignSignatureV4 - presigned signature for PostPolicy
// requests.
// func PostPresignSignatureV4(policyBase64 string, t time.Time, secretAccessKey, location string) string {
// 	// Get signining key.
// 	signingkey := getSigningKey(secretAccessKey, location, t)
// 	// Calculate signature.
// 	signature := getSignature(signingkey, policyBase64)
// 	return signature
// }

// SignV4 sign the request before Do(), in accordance with
// http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html.
func SignV4(req *http.Request, accessKeyID, secretAccessKey, sessionToken, lo string) {
	// Signature calculation is not needed for anonymous credentials.
	if accessKeyID == "" || secretAccessKey == "" {
		return
	}

	t := req.Header.Get(AmzDate)
	sv, ac := parseSignV4(req.Header.Get(Authorization), DefaultZone)

	if ac > 0 {
		return
	}

	// Get canonical request.
	canonicalRequest := getCanonicalRequest(*req, sv.SignedHeaders, sv.SignedHeaderValue)

	// Get string to sign from canonical request.
	stringToSign := getStringToSignV4(t, sv.Credential.scope.region, sv.Credential.scopeWithoutAk, canonicalRequest)

	// Get hmac signing key.
	signingKey := getSigningKey(secretAccessKey, sv.Credential.scope.region, sv.Credential.scope.dateStr)

	// Get credential string.

	// Calculate signature.
	signature := getSignature(signingKey, stringToSign)

	// If regular request, construct the final authorization header.
	parts := []string{
		signV4Algorithm + " Credential=" + sv.Credential.scopeStr,
		sv.SignedHeaderStr,
		"Signature=" + signature,
	}

	// Set authorization header.
	auth := strings.Join(parts, ", ")
	req.Header.Set(Authorization, auth)
}

func ValidateSignV4(req *http.Request, accessKeyID, secretAccessKey string) bool {
	cloneReq := req.Clone(req.Context())
	// Signature calculation is not needed for anonymous credentials.
	if accessKeyID == "" || secretAccessKey == "" {
		return false
	}

	originSign := cloneReq.Header.Get(Authorization)

	t := cloneReq.Header.Get(AmzDate)
	sv, ac := parseSignV4(originSign, DefaultZone)

	if ac > 0 {
		return false
	}

	// Get canonical request.
	canonicalRequest := getCanonicalRequest(*cloneReq, sv.SignedHeaders, sv.SignedHeaderValue)

	// Get string to sign from canonical request.
	stringToSign := getStringToSignV4(t, sv.Credential.scope.region, sv.Credential.scopeWithoutAk, canonicalRequest)

	// Get hmac signing key.
	signingKey := getSigningKey(secretAccessKey, sv.Credential.scope.region, sv.Credential.scope.dateStr)

	// Get credential string.

	// Calculate signature.
	signature := getSignature(signingKey, stringToSign)

	// If regular request, construct the final authorization header.
	parts := []string{
		signV4Algorithm + " Credential=" + sv.Credential.scopeStr,
		sv.SignedHeaderStr,
		"Signature=" + signature,
	}

	// Set authorization header.
	return originSign == strings.Join(parts, ", ")
}

// getHostAddr returns host header if available, otherwise returns host from URL
func getHostAddr(req *http.Request) string {
	if req.Host != "" {
		return req.Host
	}
	return req.URL.Host
}

// sum256 calculate sha256 sum for an input byte array.
func sum256(data []byte) []byte {
	hash := sha256.New()
	hash.Write(data)
	return hash.Sum(nil)
}

// EncodePath encode the strings from UTF-8 byte representations to HTML hex escape sequences
//
// This is necessary since regular url.Parse() and url.Encode() functions do not support UTF-8
// non english characters cannot be parsed due to the nature in which url.Encode() is written
//
// This function on the other hand is a direct replacement for url.Encode() technique to support
// pretty much every UTF-8 character.
func EncodePath(pathName string) string {
	var encodedPathname string
	for _, s := range pathName {
		if 'A' <= s && s <= 'Z' || 'a' <= s && s <= 'z' || '0' <= s && s <= '9' { // §2.3 Unreserved characters (mark)
			encodedPathname = encodedPathname + string(s)
			continue
		}
		switch s {
		case '-', '_', '.', '~', '/': // §2.3 Unreserved characters (mark)
			encodedPathname = encodedPathname + string(s)
			continue
		default:
			len := utf8.RuneLen(s)
			if len < 0 {
				// if utf8 cannot convert return the same string as is
				return pathName
			}
			u := make([]byte, len)
			utf8.EncodeRune(u, s)
			for _, r := range u {
				hex := hex.EncodeToString([]byte{r})
				encodedPathname = encodedPathname + "%" + strings.ToUpper(hex)
			}
		}
	}
	return encodedPathname
}

func sumHMAC(key []byte, data []byte) []byte {
	hash := hmac.New(sha256.New, key)
	hash.Write(data)
	return hash.Sum(nil)
}
